// // Copyright (c) 2009 All Right Reserved // // Leslie Sanford // jabberdabber@hotmail.com // 2009-01-01 // Contains ... // Free license. using System; using System.Diagnostics.Contracts; using JetBrains.Annotations; using LargoCommon.Abstract; using LargoCommon.Music; namespace LargoCommon.Midi { /// /// Provides basic functionality for generating tick events with pulses per period. /// (originally used for quarter musical note resolution, here for bars...) /// Generates clock events internally. /// public sealed class MidiClock : IDisposable { //// IClock, #region Fields /// /// Singleton variable. /// private static readonly MidiClock InternalSingleton = new MidiClock(1); /// /// Used for generating tick events. /// private readonly SimpleTimer timer; //// = new SimpleTimer(); /// /// Is disposed. /// private bool disposed; #endregion #region Constructors /// /// Initializes a new instance of the class. /// [UsedImplicitly] public MidiClock() { } /// /// Initializes a new instance of the MidiClock class. /// /// Timer Period - period in milliseconds. public MidiClock(int givenPeriod) { if (givenPeriod < 1) { throw new ArgumentOutOfRangeException(nameof(givenPeriod), givenPeriod, "Timer period cannot be less than one."); } /* Description of Midi Clock Notes are measured in pulses rather than in time. The note start time and duration will be a discrete number of pulses. Pulses per quarter note can be found in the header part of the MIDI file and specifies how many midi pulses make up one quarter note (=1/4). This is useful for determining note values - if the current pulse is 96 and a note's duration is 48 we know that it's a quaver (=1/8). It's also vital for determining the rate at which the MIDI file should be played. The tempo tells us how long each pulse should last. The BPM measures how many quarter notes happen in a minute. To work out the length of each pulse we can use the following formula: Pulse Length = 60/(BPM * pulse) MidiTempoBaseNumber = 60000000; MidiMaxTempoValue = 0xFFFFFF int Tempo [BPM] = MidiTempoBaseNumber / value [0 to MidiMaxTempoValue] //// The default tempo in microseconds: 120bpm, value = 500000 The minimum pulses per quarter note value. PulsesMinValue = 24; Pulse Length = 60/(Tempo[BPM] * PulsesMinValue[pulses]) MicrosecondsPerMillisecond = 1000; //// The number of microseconds per millisecond. */ this.timer = new SimpleTimer { Mode = TimerMode.Periodic, Period = givenPeriod, Resolution = 1 }; //// Period = 1700 this.timer.Tick += this.TimerTick; //// 2019/02 this.timer.Start(); } #endregion #region Events /// /// EventHandler Tick. /// public event EventHandler Tick; /// /// EventHandler Started. /// [UsedImplicitly] public event EventHandler Started; /// /// EventHandler Continued. /// [UsedImplicitly] public event EventHandler Continued; /// /// EventHandler Stopped. /// [UsedImplicitly] public event EventHandler Stopped; #endregion #region Properties /// /// Gets the MidiClock Singleton. /// /// Property description. public static MidiClock Singleton { get { Contract.Ensures(Contract.Result() != null); if (InternalSingleton == null) { throw new InvalidOperationException("Singleton Midi Clock is null."); } return InternalSingleton; } } /// /// Gets Tick accumulator. /// /// Property description. public int Ticks { get; private set; } /// /// Gets a value indicating whether Is Running. IClock Members. /// /// General property. public bool IsRunning { get; private set; } #endregion #region Public methods /* MIDIClock - tempo !? /// /// Sets the tempo. /// /// The given tempo. /// Tempo out of range. public void SetTempo(int givenTempo) { //// Require if (tempo < 1) { throw new ArgumentOutOfRangeException( "Tempo out of range."); } this.tempo = givenTempo; } */ /// /// Starts the MidiInternalClock. /// public void Start() { if (this.IsRunning) { return; } this.Ticks = 0; this.OnStarted(EventArgs.Empty); // Start the multimedia timer in order to start generating ticks. this.timer.Start(); // Indicate that the clock is now running. this.IsRunning = true; } /// /// Resumes tick generation from the current position. /// [UsedImplicitly] public void Continue() { if (this.IsRunning) { return; } //// Raise Continued event. this.OnContinued(EventArgs.Empty); //// Start multimedia timer in order to start generating ticks. this.timer.Start(); //// Indicate that the clock is now running. this.IsRunning = true; } /// /// Stops the MidiInternalClock. /// public void Stop() { if (!this.IsRunning) { return; } //// Stop the multimedia timer. this.timer.Stop(); //// Indicate that the clock is not running. this.IsRunning = false; this.OnStopped(EventArgs.Empty); } /// /// Rewinds this instance. /// public void Rewind() { this.Ticks = this.timer.Period; this.OnTick(EventArgs.Empty); } /// /// Skips the backward. /// /// The quotient. public void SkipBackward(int quotient) { this.Ticks -= quotient * this.timer.Period; this.Ticks = Math.Max(this.Ticks, this.timer.Period); this.OnTick(EventArgs.Empty); } /// /// Skips the forward. /// /// The quotient. public void SkipForward(int quotient) { this.Ticks += quotient * this.timer.Period; this.OnTick(EventArgs.Empty); } #endregion /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { this.Dispose(true); //// Unregister object for finalization. //// GC.SuppressFinalize(this); } /// /// Method On Continued. /// /// Event Args. [UsedImplicitly] private void OnContinued(EventArgs e) { var handler = this.Continued; handler?.Invoke(this, e); } #region IDisposable Implementation /// /// Releases unmanaged and - optionally - managed resources. /// /// Disposing true to release both managed and unmanaged resources; false to release only unmanaged resources. private void Dispose(bool disposing) { lock (this) { //// Do nothing if the object has already been disposed of. if (this.disposed) { return; } if (disposing) { //// Release disposable objects used by this instance here. this.timer?.Dispose(); InternalSingleton?.Dispose(); } // Release unmanaged resources here. Don't access reference type fields. // Remember that the object has been disposed of. this.disposed = true; } } #endregion #region Protected methods /// /// Method On Tick. /// /// Event Args. [UsedImplicitly] private void OnTick(EventArgs e) { var handler = this.Tick; //// handler.GetInvocationList().Length; handler?.Invoke(this, EventArgs.Empty); } /// /// Method On Started. /// /// Event Args. private void OnStarted(EventArgs e) { var handler = this.Started; handler?.Invoke(this, e); } /// /// Method On Stopped. /// /// Event Args. private void OnStopped(EventArgs e) { var handler = this.Stopped; handler?.Invoke(this, e); } #endregion #region Private methods /// /// Timer Tick. /// /// Sender object. /// Event Arguments. private void TimerTick(object sender, EventArgs e) { //// virtual Contract.Requires(this.timer != null); this.Ticks += this.timer.Period; this.OnTick(e); } #endregion } }